use anyhow::{anyhow, Context};
use git2::build::{CheckoutBuilder, RepoBuilder};
use git2::{
    Branch, CertificateCheckStatus, FetchOptions, Remote, RemoteCallbacks, Repository,
    RepositoryState,
};
use lazy_static::lazy_static;
use napi_derive_ohos::napi;
use napi_ohos::bindgen_prelude;
use std::path::Path;
use std::sync::Arc;
use tokio::sync::mpsc::UnboundedSender;
use tokio::sync::Mutex;

const REPO_URL: &str = "https://gitcode.com/niuhuan_cn/genshin_wiki_data.git";

lazy_static! {
    static ref last_msg: Mutex<String> = Mutex::new("".to_string());
}

#[napi]
pub async fn init_root(root_path: String) -> napi_ohos::Result<()> {
    crate::context::init_root(&root_path).await;
    Ok(())
}

#[napi]
pub fn repo_state() -> napi_ohos::Result<String> {
    let repo_dir = crate::context::repo_dir();
    Ok(if std::fs::exists(repo_dir).map_err(crate::map_anyhow)? {
        match Repository::init(repo_dir) {
            Ok(repo) => {
                let state = repo.state();
                match state {
                    RepositoryState::Clean => "CLEAN".to_string(),
                    _ => "ERROR: REPO NOT CLEAN".to_string(),
                }
            }
            Err(err) => format!("ERROR:{}", err),
        }
    } else {
        "NotExists".to_string()
    })
}

#[napi]
pub async fn get_last_msg() -> napi_ohos::Result<String> {
    let lock = last_msg.lock().await;
    Ok(lock.clone())
}

#[napi]
pub async fn re_clone() -> napi_ohos::Result<String> {
    let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::<String>();
    let jh = std::thread::spawn(move || {
        let sender = Arc::new(sender);
        let repo_dir = crate::context::repo_dir();
        if std::fs::exists(repo_dir).is_ok() {
            let _ = std::fs::remove_dir_all(repo_dir);
        }
        let mut cb = RemoteCallbacks::new();
        cb.certificate_check(|_, _| Ok(CertificateCheckStatus::CertificateOk));
        let ass = sender.clone();
        cb.transfer_progress(move |stats| {
            let dc = stats.indexed_deltas();
            let oc = stats.indexed_objects();
            let msg = format!("indexed [ deltas: {}, objects: {} ]", dc, oc);
            let _ = ass.send(msg);
            true
        });
        let ass = sender.clone();
        let mut co = CheckoutBuilder::new();
        co.progress(move |_, cur, total| {
            let msg = format!("checkout [ cur: {}, total: {} ]", cur, total);
            let _ = ass.send(msg);
        });

        let mut fo = FetchOptions::new();
        fo.remote_callbacks(cb);
        let result = match RepoBuilder::new()
            .fetch_options(fo)
            .with_checkout(co)
            .clone(REPO_URL, Path::new(repo_dir))
        {
            Ok(_) => "OK".to_string(),
            Err(err) => format!("ERROR:{}", err),
        };
        let _ = sender.send(result);
    });
    let mut msg: String = "".to_string();
    while let Some(_msg) = receiver.recv().await {
        msg = _msg;
        let mut lock = last_msg.lock().await;
        *lock = msg.clone();
        drop(lock);
    }
    Ok(msg)
}

#[napi]
pub async fn repo_pull() -> napi_ohos::Result<String> {
    let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::<String>();
    let jh = std::thread::spawn(move || fetch_inner(sender));
    while let Some(_msg) = receiver.recv().await {
        let mut lock = last_msg.lock().await;
        *lock = _msg;
        drop(lock);
    }
    let result = jh.join().unwrap();
    Ok(match (result) {
        Ok(_) => "OK".to_string(),
        Err(err) => {
            format!("ERROR:{}", err)
        }
    })
}

fn fetch_inner(callback: UnboundedSender<String>) -> napi_ohos::Result<()> {
    let callback = Arc::new(callback);
    let repo_dir = crate::context::repo_dir();
    let repo = Repository::open(repo_dir).map_err(crate::map_anyhow)?;
    /*
       Branch: Local: "master"
       Branch: Remote: "origin/HEAD"
       Branch: Remote: "origin/master"
    */
    let mut pull_branch: Option<Branch> = None;
    let mut branches = repo.branches(None).map_err(crate::map_anyhow)?;
    while let Some(branch) = branches.next() {
        let (branch, _) = branch.map_err(crate::map_anyhow)?;
        if branch.is_head() {
            pull_branch = Some(branch);
            break;
        }
    }
    /*
    Remote: Some("origin")
    */
    let mut pull_remote: Option<Remote> = None;
    let remotes = repo.remotes().map_err(crate::map_anyhow)?;
    for remote in remotes.iter() {
        if let Some(remote) = remote {
            let remote = repo.find_remote(remote).expect("remote not found");
            pull_remote = Some(remote);
            break;
        }
    }
    //
    let branch = pull_branch
        .with_context(|| anyhow!("branch not found"))
        .map_err(crate::map_anyhow)?;
    let branch = branch
        .name()
        .map_err(crate::map_anyhow)?
        .with_context(|| anyhow!(""))
        .map_err(crate::map_anyhow)?;
    let mut remote = pull_remote
        .with_context(|| anyhow!("remote not found"))
        .map_err(crate::map_anyhow)?;
    // Fetch
    let mut cb = git2::RemoteCallbacks::new();
    cb.certificate_check(|_, _| Ok(CertificateCheckStatus::CertificateOk));
    let ass = callback.clone();
    cb.transfer_progress(move |stats| {
        let dc = stats.indexed_deltas();
        let oc = stats.indexed_objects();
        let msg = format!("indexed [ deltas: {}, objects: {} ]", dc, oc);
        let _ = ass.send(msg);
        true
    });
    let mut fo = FetchOptions::new();
    fo.remote_callbacks(cb);
    fo.download_tags(git2::AutotagOption::None);
    remote
        .fetch(&[branch], Some(&mut fo), None)
        .map_err(crate::map_anyhow)?;
    let fetch_head = repo
        .find_reference("FETCH_HEAD")
        .map_err(crate::map_anyhow)?;
    let fetch_commit = repo
        .reference_to_annotated_commit(&fetch_head)
        .map_err(crate::map_anyhow)?;
    let analysis = repo
        .merge_analysis(&[&fetch_commit])
        .map_err(crate::map_anyhow)?;
    if analysis.0.is_fast_forward() {
        println!("Doing a fast forward");
        let refname = format!("refs/heads/{}", branch);
        match repo.find_reference(&refname) {
            Ok(mut r) => {
                fast_forward(&repo, &mut r, &fetch_commit).map_err(crate::map_anyhow)?;
            }
            Err(_) => {
                // The branch doesn't exist so just set the reference to the
                // commit directly. Usually this is because you are pulling
                // into an empty repository.
                repo.reference(
                    &refname,
                    fetch_commit.id(),
                    true,
                    &format!("Setting {} to {}", branch, fetch_commit.id()),
                )
                .map_err(crate::map_anyhow)?;
                repo.set_head(&refname).map_err(crate::map_anyhow)?;
                repo.checkout_head(Some(
                    git2::build::CheckoutBuilder::default()
                        .allow_conflicts(true)
                        .conflict_style_merge(true)
                        .force(),
                ))
                .map_err(crate::map_anyhow)?;
            }
        };
    } else if analysis.0.is_normal() {
        // do a normal merge
        let head_commit = repo
            .reference_to_annotated_commit(&repo.head().map_err(crate::map_anyhow)?)
            .map_err(crate::map_anyhow)?;
        normal_merge(&repo, &head_commit, &fetch_commit).map_err(crate::map_anyhow)?;
    } else {
        println!("Nothing to do...");
    }
    Ok(())
}

fn fast_forward(
    repo: &Repository,
    lb: &mut git2::Reference,
    rc: &git2::AnnotatedCommit,
) -> Result<(), git2::Error> {
    let name = match lb.name() {
        Some(s) => s.to_string(),
        None => String::from_utf8_lossy(lb.name_bytes()).to_string(),
    };
    let msg = format!("Fast-Forward: Setting {} to id: {}", name, rc.id());
    println!("{}", msg);
    lb.set_target(rc.id(), &msg)?;
    repo.set_head(&name)?;
    repo.checkout_head(Some(
        git2::build::CheckoutBuilder::default()
            // For some reason the force is required to make the working directory actually get updated
            // I suspect we should be adding some logic to handle dirty working directory states
            // but this is just an example so maybe not.
            .force(),
    ))?;
    Ok(())
}

fn normal_merge(
    repo: &Repository,
    local: &git2::AnnotatedCommit,
    remote: &git2::AnnotatedCommit,
) -> Result<(), git2::Error> {
    let local_tree = repo.find_commit(local.id())?.tree()?;
    let remote_tree = repo.find_commit(remote.id())?.tree()?;
    let ancestor = repo
        .find_commit(repo.merge_base(local.id(), remote.id())?)?
        .tree()?;
    let mut idx = repo.merge_trees(&ancestor, &local_tree, &remote_tree, None)?;

    if idx.has_conflicts() {
        println!("Merge conflicts detected...");
        repo.checkout_index(Some(&mut idx), None)?;
        return Ok(());
    }
    let result_tree = repo.find_tree(idx.write_tree_to(repo)?)?;
    // now create the merge commit
    let msg = format!("Merge: {} into {}", remote.id(), local.id());
    let sig = repo.signature()?;
    let local_commit = repo.find_commit(local.id())?;
    let remote_commit = repo.find_commit(remote.id())?;
    // Do our merge commit and set current branch head to that commit.
    let _merge_commit = repo.commit(
        Some("HEAD"),
        &sig,
        &sig,
        &msg,
        &result_tree,
        &[&local_commit, &remote_commit],
    )?;
    // Set working tree to match head.
    repo.checkout_head(None)?;
    Ok(())
}

#[napi]
pub fn repo_dir() -> napi_ohos::Result<String> {
    Ok(crate::context::repo_dir().clone())
}
